package org.mzx.shortLink.work.service.impl;


import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jodd.util.StringUtil;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;

import org.mzx.shortLink.starter.convention.Exception.ClientException;
import org.mzx.shortLink.starter.convention.Exception.ServiceException;
import org.mzx.shortLink.work.common.constant.rabbitMq.rabbitMqShortLinkStatusDto;
import org.mzx.shortLink.work.domain.entity.*;
import org.mzx.shortLink.work.domain.mapper.*;
import org.mzx.shortLink.work.dto.Request.TLinkCountDtoReq;
import org.mzx.shortLink.work.dto.Request.TLinkCreateDtoReq;

import org.mzx.shortLink.work.dto.Request.TLinkUpdateDtoReq;
import org.mzx.shortLink.work.dto.Response.TLinkCreateDtoResp;
import org.mzx.shortLink.work.dto.Response.TLinkPageDtoResp;
import org.mzx.shortLink.work.mq.producer.shortLinkStatusSaveProducer;
import org.mzx.shortLink.work.service.TLinkGotoService;
import org.mzx.shortLink.work.service.TLinkService;
import org.mzx.shortLink.work.utils.HashUtil;
import org.mzx.shortLink.work.utils.LinkUtil;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.sql.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import static org.mzx.shortLink.work.common.enums.RedisData.*;

/**
 * @author mazhenxi
 * @description 针对表【t_link】的数据库操作Service实现
 * @createDate 2024-11-27 17:46:02
 */
@Service
@RequiredArgsConstructor
public class TLinkServiceImpl extends ServiceImpl<TLinkMapper, TLink>
        implements TLinkService {

    private final RBloomFilter<String> LinkCreateCachePenetrationBloomFilter;

    private final TLinkMapper mapper;

    private final TLinkGotoService gotoService;

    private final StringRedisTemplate stringRedisTemplate;

    private final RedissonClient redissonClient;



    @Value("${short-link.statsLocaleAmapKey}")
    public String statsLocaleAmapKey;



    private final shortLinkStatusSaveProducer shortLinkStatusSaveProducer;
    @Autowired
    @Lazy
    private TLinkServiceImpl tLinkService;

    @Value("${short-link.domain}")
    private String ShortLinkDomain;
    @Override
    @Transactional
    //因为需要同时更新两个表，所以需要添加事务注解
    public TLinkCreateDtoResp create(TLinkCreateDtoReq requestParam) {
        String urlSuffix = checkRepeat(requestParam);
        //创建对象保存到数据库中
        TLink link = BeanUtil.toBean(requestParam, TLink.class);
        link.setDomain(ShortLinkDomain);
        link.setFullShortUrl(ShortLinkDomain + "/" + urlSuffix);
        link.setShortUri(urlSuffix);
        link.setEnableStatus(0);
        //添加数据到路由表
        TLinkGoto Goto = TLinkGoto.builder()
                .gid(link.getGid())
                .fullShortUrl(link.getFullShortUrl())
                .build();
        //添加到数据库中
        try {
            save(link);
            gotoService.save(Goto);
            stringRedisTemplate.opsForValue().set(SHORT_LINK_KEY + link.getFullShortUrl(), link.getOriginUrl(), LinkUtil.getDate(link), TimeUnit.MILLISECONDS);
        } catch (Exception e) {
            //这里是多个线程同时拿到了相同的url且通过了布隆过滤器，插入到数据库中了，但是数据库中有索引
            log.debug("多个线程同时拿到了相同url且进入了数据库");
            throw new ClientException("多个线程同时拿到了相同url且进入了数据库");
        }

        //返回给前端的包装类
        return BeanUtil.toBean(link, TLinkCreateDtoResp.class);
    }

    @Override
    public IPage<TLinkPageDtoResp> getByPage(String gid, Page<TLink> pageType,String orderTag) {
        Date date = new Date(System.currentTimeMillis());
        return mapper.getByPage(gid,pageType,orderTag,date);
    }

    @Override
    public Map<String, Map<String, Object>> Count(List<TLinkCountDtoReq> requestParam) {
        Map<String, Map<String, Object>> count = mapper.Count(requestParam);
        if (count.size() == 0 || count == null) {
            requestParam.forEach(each -> {
                count.put(each.getGid(), null);
            });
        }
        return count;
    }

    @Override
    public Boolean Update(TLinkUpdateDtoReq requestParam) {
        //获取读写锁
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(SHORT_LINK_UPDATE_LOCK_KEY + requestParam.getFullShortUrl());
        //这里是更新所以是写锁
        boolean update = false;
        RLock rLock = readWriteLock.writeLock();
        rLock.lock();
        try {
            update = lambdaUpdate().eq(TLink::getGid, requestParam.getOriginGid())
                    .eq(TLink::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(TLink::getEnableStatus, 0)
                    .set(TLink::getDescribe, requestParam.getDescribe())
                    .set(TLink::getGid, requestParam.getGid())
                    .set(requestParam.getValidDateType().equals(0), TLink::getValidDate, null)
                    .update();
        }catch (Exception e) {
            log.debug(e.getMessage());
        }finally {
            rLock.unlock();
        }

        return Boolean.valueOf(update);
    }

    @SneakyThrows
    @Override
    public void reDirect(String shortUrl, HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
        //首先从布隆过滤器中去查我们传来的短链接是否存在
        //这里是获取域名
        String domain = httpRequest.getServerName();
        String fullUrl = domain + ":8001/" + shortUrl;
        //我们在查询布隆过滤器之前先查询我们缓存中的数据,防止布隆过滤器的误判
        //缓存击穿
        String originalLink = stringRedisTemplate.opsForValue().get(SHORT_LINK_KEY + fullUrl);
        if (StringUtil.isNotBlank(originalLink)) {
            //如果在缓存中查到了数据，即我们存的原始链接，直接跳转
            tLinkService.buildLinkStatsRecordAndSetUser(fullUrl, httpRequest, httpResponse);
            httpResponse.sendRedirect(originalLink);
            return;
        }
        //如果缓存中没有查到就查布隆过滤器，看其是否存在
        //也是解决缓存穿透问题
        boolean contains = LinkCreateCachePenetrationBloomFilter.contains(fullUrl);
        if (!contains) {
            //不存在的情况，布隆过滤器查到是不存在那么一定不存在
            httpResponse.sendRedirect("/page/notfound");
            return;
        }
        //如果走到这里可能有两种情况，一就是缓存失效了且真的存在，二就是布隆过滤器误判了，根本就不存在
        //查询我们路由表中的缓存--这里是解决缓存穿透问题！！！
        String gotoIsNUll = stringRedisTemplate.opsForValue().get(GOTO_ISNULL_SHORT_LINK_KEY + fullUrl);
        if (StringUtil.isNotBlank(gotoIsNUll)) {
            httpResponse.sendRedirect("/page/notfound");
            return;
        }
        //到了这就说明真的存在改短连接了，就是缓存过期了，解决缓存击穿
        //获取分布式锁
        RLock lock = redissonClient.getLock(LOCK_SHORT_LINK_KEY + fullUrl);
        boolean tryLock = lock.tryLock(10, TimeUnit.SECONDS);
        //尝试获取锁，tryLock有重试机制
        if (tryLock) {
            //得到了分布式锁
            try {
                //进行二重锁判定，减轻数据库的压力
                originalLink = stringRedisTemplate.opsForValue().get(SHORT_LINK_KEY + fullUrl);
                if (StringUtil.isNotBlank(originalLink)) {
                    //如果在缓存中查到了数据，即我们存的原始链接，直接跳转
                    tLinkService.buildLinkStatsRecordAndSetUser(fullUrl, httpRequest, httpResponse);
                    httpResponse.sendRedirect(originalLink);
                    return;
                }
                gotoIsNUll = stringRedisTemplate.opsForValue().get(GOTO_ISNULL_SHORT_LINK_KEY + fullUrl);
                if (StringUtil.isNotBlank(gotoIsNUll)) {
                    httpResponse.sendRedirect("/page/notfound");
                    return;
                }

                //开始真正查询数据库，多个并发数据只有一个可以走到这里
                TLinkGoto Goto = gotoService.lambdaQuery().eq(TLinkGoto::getFullShortUrl, fullUrl)
                        .one();
                if (Goto == null) {
                    //空值法处理缓存穿透
                    stringRedisTemplate.opsForValue().set(GOTO_ISNULL_SHORT_LINK_KEY + fullUrl, "1", 30, TimeUnit.MINUTES);
                    httpResponse.sendRedirect("/page/notfound");
                    return;
                }
                TLink link = lambdaQuery().eq(TLink::getGid, Goto.getGid())
                        .eq(TLink::getFullShortUrl, fullUrl)
                        .eq(TLink::getEnableStatus, 0)
                        .one();
                if (link == null || (LinkUtil.getDate(link) < 0 && link.getValidDateType().equals(1))) {
                    stringRedisTemplate.opsForValue().set(GOTO_ISNULL_SHORT_LINK_KEY + fullUrl, "1", 30, TimeUnit.MINUTES);
                    httpResponse.sendRedirect("/page/notfound");
                    return;
                }
                //重新从数据库中获取到了数据，把此数据重新放入缓存中
                stringRedisTemplate.opsForValue().set(SHORT_LINK_KEY + fullUrl, link.getOriginUrl(), LinkUtil.getDate(link), TimeUnit.MILLISECONDS);
                //进行监控记录
                //防止事务失效，自己注入自己，调用代理对象的方法
                tLinkService.buildLinkStatsRecordAndSetUser(fullUrl, httpRequest, httpResponse);
                //进行重定向
                httpResponse.sendRedirect(link.getOriginUrl());

            } catch (Exception e) {
                log.debug(e.getMessage());
            } finally {
                //释放锁
                lock.unlock();
            }
        }
    }

    @Transactional
    public void buildLinkStatsRecordAndSetUser(String fullUrl, HttpServletRequest httpRequest, HttpServletResponse httpResponse) {


        Cookie[] cookies = httpRequest.getCookies();
        String cookieValue = "";
        boolean cookieValueExist = false;
        for (Cookie cookie : cookies) {
            if (cookie.getName().equals("StatsRecords")) {
                cookieValueExist = true;
                break;
            }
        }
        //这里是给那些不登陆就访问网址的人做一个统计
        if (!cookieValueExist) {
            cookieValue = String.valueOf(UUID.randomUUID());
            Cookie cookie = new Cookie("StatsRecords", cookieValue);
            cookie.setPath(StrUtil.sub(fullUrl, fullUrl.indexOf("/"), fullUrl.length()));
            //cookie.setPath("/");
            cookie.setMaxAge((24 - LocalDateTime.now().getHour()) * 60 * 60);
            httpResponse.addCookie(cookie);
        } else {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals("StatsRecords")) {
                    cookieValue = cookie.getValue();
                    break;
                }
            }
        }
        //这里我们得到了cookieValue可以把他加入统计了,每一个cookie代表一个用户id
        LocalDateTime current = LocalDateTime.now();
        rabbitMqShortLinkStatusDto statusDto = rabbitMqShortLinkStatusDto.builder()
                .day(current.getDayOfMonth())
                .year(current.getYear())
                .ip(LinkUtil.getActualIp(httpRequest))
                .os(LinkUtil.getOs(httpRequest))
                .browser(LinkUtil.getBrowser(httpRequest))
                .device(LinkUtil.getDevice(httpRequest))
                .netWork(LinkUtil.getNetwork(httpRequest))
                .date(new Date(System.currentTimeMillis()))
                .month(current.getMonthValue())
                .fullUrl(fullUrl)
                .cookieValue(cookieValue)
                .hour(current.getHour())
                .weekDay(current.getDayOfWeek().getValue()).build();
        shortLinkStatusSaveProducer.sendShortLinkStatus(statusDto);


    }

    private String checkRepeat(TLinkCreateDtoReq requestParam) {
        String originUrl = requestParam.getOriginUrl();
        String urlSuffix;
        if (StrUtil.isBlank(originUrl)) {
            throw new ClientException("传入的链接为空");
        }
        Integer count = 0;
        //布隆过滤器有误判，是把不存在判断为存在
        //这里的for循环就是解决误判把不存在判断为存在
        while (true) {
            //一共有十次hash冲突的机会，可以设置的少一点，但是如果多的话，遭到恶意攻击会造成数据库压力过大
            if (count > 10) {
                throw new ServiceException("短链接生成过于频繁,稍后再试");
            }

            //通过原始链接加上当前的时间戳，要不然每次循环都是根据同一条原始链接进行hash操作，永远都是得到的同一条，白循环
            urlSuffix = HashUtil.convertDecToBase62(originUrl + System.currentTimeMillis());
            //获取完整的链接
            String fullUrl = requestParam.getDomain() + ":8001/" + urlSuffix;
            //布隆过滤器查重
            boolean contains = LinkCreateCachePenetrationBloomFilter.contains(fullUrl);
            if (!contains) {
                LinkCreateCachePenetrationBloomFilter.add(fullUrl);
                break;
            }
            count++;
        }

        return urlSuffix;
    }
}




